<?php

 /**
  * @package JooS
  * @subpackage PHPackager
  */

  class JooS_PHPackager_Manager {
    const PHPACKAGE_XML = "phpackage.xml";

    const STAGE_ADD_PRE_INSTALL = "pre-install";

    const STAGE_ADD_PRE_UPGRADE = "pre-upgrade";

    const STAGE_ADD_INSTALL = "install";

    const STAGE_ADD_POST_INSTALL = "post-install";

    const STAGE_ADD_POST_UPGRADE = "post-upgrade";

    const STAGE_ADD_FAILED_INSTALL = "failed-install";

    const STAGE_DELETE_PRE_REMOVE = "pre-remove";

    const STAGE_DELETE_PRE_UPGRADE = "pre-upgrade";

    const STAGE_DELETE_REMOVE = "remove";

    const STAGE_DELETE_POST_REMOVE = "post-remove";

    const STAGE_DELETE_POST_UPGRADE = "post-upgrade";

    const STAGE_DELETE_ABORT_UPGRADE = "abort-upgrade";

    const CONFIG_PACKAGES_LIST = "PHPackager.List";
    
    const EVENT_PACKAGE_NEW_SOURCE = "PHPackager.packageNewSource";

    const EVENT_CONFIG_COMMIT = "PHPackager.configCommit";

    const EVENT_OBSERVERS_COMMIT = "PHPackager.observersCommit";

    const EVENT_PACKAGE_NEW_INSTANCE = "PHPackager.packageNewInstance";

    const EVENT_PACKAGE_INSTALLED = "PHPackager.packageInstalled";

    const EVENT_PACKAGE_UPGRADED = "PHPackager.packageUpgraded";

    const EVENT_PACKAGE_COMMITED = "PHPackager.packageCommited";

    const EVENT_PACKAGER_BEFORE_ADD = "PHPackager.packagerBeforeAdd";
    
    const EVENT_PACKAGER_AFTER_ADD = "PHPackager.packagerAfterAdd";

    const EVENT_PACKAGER_BEFORE_REMOVE = "PHPackager.packagerBeforeRemove";

    const EVENT_PACKAGER_AFTER_REMOVE = "PHPackager.packagerAfterRemove";

    const EVENT_PACKAGER_INCLUDEPATH = "PHPackager.includePath";

    const OBSERVER_FIND_PACKAGE = "Observer_findPackage";

    /**
     * @var array
     */
    private static $_installedPackages = null;

    /**
     * @var array
     */
    private $_newPackages = array();

    /**
     * @param string $dir
     */
    public static function addPackages($dir) {
      require_once "JooS/Event.php";

      if (!JooS_Event::newInstance(self::EVENT_PACKAGER_BEFORE_ADD)->notify())
        return;

      try {
        self::startTransaction();

        $manager = new static();
        $manager
          ->make($dir)
          ->install();

        self::commitTransaction();
      }
      catch (Exception $e) {
        self::rollbackTransaction();
        throw $e;
      }

      JooS_Event::newInstance(self::EVENT_PACKAGER_AFTER_ADD)->notify();
    }

    /**
     * @param string $name
     */
    public static function removePackage($name) {
      require_once "JooS/Event.php";

      if (!JooS_Event::newInstance(self::EVENT_PACKAGER_BEFORE_REMOVE)->notify())
        return;

      try {
        self::startTransaction();

        require_once "JooS/Syntax/Compiler.php";
        require_once "JooS/Syntax/PHPackager/DOM/Name.php";

        $systemName = JooS_Syntax_Compiler::newInstance( new JooS_Syntax_PHPackager_DOM_Name() )
          ->compile($name);

        if ($systemName === false)
          self::throwException("Bad package name '$name'");

        $manager = new static();
        $result = $manager->remove($systemName);

        self::commitTransaction();
      }
      catch (Exception $e) {
        self::rollbackTransaction();
        throw $e;
      }

      JooS_Event::newInstance(self::EVENT_PACKAGER_AFTER_ADD)->notify($result);
    }

    /**
     * 
     */
    protected static function startTransaction() {
      set_error_handler(function($errno, $errstr, $errfile) {
        require_once "JooS/PHPackager/Exception.php";

        throw new JooS_PHPackager_Exception("PHP: '$errstr' in '$errfile'", $errno);
      }, E_ALL | E_STRICT | E_RECOVERABLE_ERROR | E_DEPRECATED | E_USER_DEPRECATED);

      require_once "JooS/Event.php";
      require_once "JooS/PHPackager/FS.php";

      JooS_PHPackager_FS::startTransaction();

      JooS_Event::newInstance(self::EVENT_CONFIG_COMMIT)
        ->clear()
        ->attach(array(__CLASS__, "configCommit"), true);

      JooS_Event::newInstance(self::EVENT_OBSERVERS_COMMIT)
        ->clear()
        ->attach(array(__CLASS__, "observersCommit"), true);
    }

    /**
     * 
     */
    protected static function commitTransaction() {
      self::checkIntegrity();

      require_once "JooS/Event.php";
      require_once "JooS/PHPackager/FS.php";

      JooS_Event::newInstance(self::EVENT_CONFIG_COMMIT)
        ->notify(self::CONFIG_PACKAGES_LIST, self::$_installedPackages->valueOf());

      JooS_Event::newInstance(self::EVENT_CONFIG_COMMIT)
        ->rollback();

      JooS_PHPackager_FS::commitTransaction();

      restore_error_handler();
    }

    /**
     * 
     */
    protected static function rollbackTransaction() {
      restore_error_handler();
    }

    /**
     *
     * @param string $name
     * @param mixed $data
     */
    public static function configCommit($name, $data) {
      require_once "JooS/PHPackager/FS.php";

      $fileSystem = JooS_PHPackager_FS::getInstance();
      if ($fileSystem) {
        require_once "JooS/Config.php";
        require_once "JooS/Loader.php";
        require_once "JooS/CodeGenerator/Class.php";
        require_once "JooS/CodeGenerator/Method.php";
        require_once "JooS/CodeGenerator/Property.php";

        $className = JooS_Config::getClassName($name);

        $configData = JooS_CodeGenerator_Property::newInstance("_staticData")
          ->setStatic(true)
          ->setVisibility( JooS_CodeGenerator_Property::VISIBILITY_PRIVATE )
          ->setValue($data);

        $configConstructor = JooS_CodeGenerator_Method::newInstance("__construct")
          ->setVisibility( JooS_CodeGenerator_Method::VISIBILITY_PROTECTED )
          ->setBody("      parent::__construct(self::\$_staticData);");

        $configClass = JooS_CodeGenerator_Class::newInstance($className)
          ->setExtendedClass("JooS_Config")
          ->addProperty($configData)
          ->addMethod($configConstructor);

        $configPath = JooS_Loader::getRelativePath($className);
        $configRealPath = JooS_Loader::getRealPath($className);

        $fileSystem->delete($configRealPath);
        $fileSystem->file_put_contents($configPath, $configClass->compile());
      }
    }

    /**
     * @param JooS_Event $Event
     */
    public static function observersCommit(JooS_Event $Event) {
      require_once "JooS/Event.php";

      JooS_Event::newInstance(self::EVENT_CONFIG_COMMIT)
        ->notify($Event->name(), $Event->observers());
    }

    /**
     *
     */
    protected static function checkIntegrity() {
      require_once "JooS/Loader.php";

      $manager = new static();

      foreach (self::$_installedPackages as $name) {
        $className = $name->valueOf();

        if (JooS_Loader::loadClass($className))
          $package = new $className();
        else
          self::throwException("Package $className not found...");

        $manager
          ->checkDepends($package)
          ->checkConflicts($package)
          ->checkBreaks($package);
      }
    }

    /**
     * 
     */
    protected function __construct() {
      if (self::$_installedPackages === null)
        self::$_installedPackages = JooS_Config::getInstance(self::CONFIG_PACKAGES_LIST);
    }

    /**
     * @param string $dir
     * @return JooS_PHPackager_Manager
     */
    protected function make($dir) {
      require_once "JooS/Loader.php";
      require_once "JooS/Config.php";
      require_once "JooS/Event.php";
      require_once "JooS/CodeGenerator/File.php";
      require_once "JooS/PHPackager/FS.php";
      require_once "JooS/PHPackager/DOM/Node.php";

      if (is_string($dir))
        $dir = array($dir);
      $path = null;

      $_packagesNew = &$this->_newPackages;

      $Event_newPackage = JooS_Event::newInstance(self::EVENT_PACKAGE_NEW_SOURCE)
        ->clear()
        ->attach(function($className, JooS_CodeGenerator_Class $package) use (&$path, &$_packagesNew) {
          $packageSource = $package->compile();

          $adapter = JooS_CodeGenerator_File::createAdapter("Tmp")
            ->file_put_contents($packageSource)
            ->include_file();

          JooS_PHPackager_FS::getInstance()->copy( $adapter->get_name(), JooS_Loader::getRelativePath($className) );

          $newPackage = new $className();
          $newPackage->setInstallPath($path);
          $newPackage->createArchive();

          $_packagesNew[$newPackage->getName()] = $newPackage;

          JooS_Event::newInstance(JooS_PHPackager_Manager::EVENT_PACKAGE_NEW_INSTANCE)
            ->notify($newPackage);
        })
        ->commit();

      foreach ($dir as $path) {
        $path = realpath($path);

        $document = new DOMDocument("1.0", "UTF-8");
        $document->load( $path . DIRECTORY_SEPARATOR . self::PHPACKAGE_XML );

        JooS_PHPackager_DOM_Node::newInstance($document->documentElement, null, "JooS_PHPackager_DOM_Element_Control")
          ->make()
          ->install();
      }
      $Event_newPackage->rollback();

      return $this;
    }

    /**
     * @return JooS_PHPackager_Manager
     */
    protected function install() {
      require_once "JooS/Loader.php";
      require_once "JooS/Config.php";
      require_once "JooS/Event.php";
      require_once "JooS/PHPackager/FS.php";
      require_once "JooS/PHPackager/Instance.php";

      $Event_configCommit = JooS_Event::newInstance(self::EVENT_CONFIG_COMMIT);

      foreach ($this->_newPackages as $className => $NEW_package) {
        $this
          ->checkBreaks($NEW_package)
          ->checkConflicts($NEW_package)
          ->checkDepends($NEW_package);

        $NEW_version = $NEW_package->getVersion();

        $OLD_package = $this->getPackageByName($NEW_package->getName());
        $OLD_version = $OLD_package ? $OLD_package->getVersion() : null;

        try {
          if ($OLD_package === null) {
            $NEW_package->add( self::STAGE_ADD_PRE_INSTALL );
            $NEW_package->add( self::STAGE_ADD_INSTALL );
            $NEW_package->add( self::STAGE_ADD_POST_INSTALL );

            JooS_Event::newInstance(self::EVENT_PACKAGE_INSTALLED)->notify($NEW_package);
          }
          else {
            $OLD_package->delete( self::STAGE_DELETE_PRE_UPGRADE, $NEW_version );
            $NEW_package->add( self::STAGE_ADD_PRE_UPGRADE, $OLD_version );
            $OLD_package->delete( self::STAGE_DELETE_REMOVE, $NEW_version );
            $OLD_package->delete( self::STAGE_DELETE_POST_UPGRADE, $NEW_version );
            $NEW_package->add( self::STAGE_ADD_INSTALL, $OLD_version );
            $NEW_package->add( self::STAGE_ADD_POST_UPGRADE, $OLD_version );

            JooS_Event::newInstance(self::EVENT_PACKAGE_UPGRADED)->notify($NEW_package, $OLD_package);
          }
        }
        catch (Exception $e) {
          restore_error_handler();

          if ($OLD_package !== null)
            $OLD_package->delete( self::STAGE_DELETE_ABORT_UPGRADE, $NEW_version );
          $NEW_package->add( self::STAGE_ADD_FAILED_INSTALL, $OLD_version );

          throw $e;
        }

        $Event_configCommit->notify( $NEW_package->getEnvironmentConfigName(), $NEW_package->getEnvironment() );
      }

      foreach ($this->_newPackages as $className => $package) {
        $realPath = $package->getArchive()->close()->getAdapter()
          ->get_name();

        JooS_PHPackager_FS::getInstance()
          ->copy($realPath, JooS_Loader::getRelativePath(get_class($package), "zip"));

        self::$_installedPackages[$package->getName()] = get_class($package);

        JooS_Event::newInstance(self::EVENT_PACKAGE_COMMITED)->notify($package);
      }

      return $this;
    }

    /**
     *
     * @param string $name
     * @param string $operator
     * @param string $version
     * @return bool
     */
    protected function remove($name, $operator = null, $version = null) {
      $OLD_package = $this->getPackageByName($name, $operator, $version);
      if ($OLD_package) {
        $OLD_package->delete( self::STAGE_DELETE_PRE_REMOVE );
        $OLD_package->delete( self::STAGE_DELETE_REMOVE );
        $OLD_package->delete( self::STAGE_DELETE_POST_REMOVE );

        unset(self::$_installedPackages[$name]);
        return true;
      }
      else
        return false;
    }

    /**
     * @param string $name
     * @param string $operator
     * @param string $version
     * @param string $installed
     * @return JooS_PHPackager_Instance
     */
    protected function getPackageByName($name, $operator = null, $version = null, $installed = true) {
      if (!$installed && isset($this->_newPackages[$name])) {
        $package = $this->_newPackages[$name];
        $className = get_class($package);
      }
      elseif (isset(self::$_installedPackages[$name])) {
        $className = self::$_installedPackages[$name]->valueOf();

        if (JooS_Loader::loadClass($className))
          $package = new $className();
        else
          self::throwException("Already installed package '$name' not found");
      }
      else
        return null;

      if ($operator !== null && $version !== null) {
        if (version_compare($package->getVersion(), $version, $operator))
          return $package;
      }
      else
        return $package;

      return null;
    }

    /**
     * @param JooS_PHPackager_Instance $package
     * @return JooS_PHPackager_Manager 
     */
    protected function checkDepends(JooS_PHPackager_Instance $package) {
      foreach ($package->getDepends() as $depends) {
        if ($depends instanceof JooS_PHPackager_Relation_Standalone) {
          if (!$depends->exists())
            self::throwException(__METHOD__ . ": " . $depends);
        }
        elseif (!$this->getPackageByName($depends->getName(), $depends->getOperator(), $depends->getVersion(), false))
          self::throwException(__METHOD__ . ": " . $depends);
      }
      return $this;
    }

    /**
     * @param JooS_PHPackager_Instance $package
     * @return JooS_PHPackager_Manager
     */
    protected function checkBreaks(JooS_PHPackager_Instance $package) {
      foreach ($package->getBreaks() as $break) {
        if ($this->getPackageByName($break->getName(), $break->getOperator(), $break->getVersion()))
          self::throwException(__METHOD__ . ": " . $break->__toString());
      }
      return $this;
    }

    /**
     * @param JooS_PHPackager_Instance $package
     * @return JooS_PHPackager_Manager
     */
    protected function checkConflicts(JooS_PHPackager_Instance $package) {
      foreach ($package->getConflicts() as $conflict) {
        if ($this->getPackageByName($conflict->getName(), $conflict->getOperator(), $conflict->getVersion()))
          self::throwException(__METHOD__ . ": " . $conflict->__toString());
      }
      return $this;
    }

    /**
     * @param string $Message
     * @param int $Code
     * @throws JooS_PHPackager_Exception
     */
    protected static function throwException($Message, $Code = 0) {
      require_once "JooS/PHPackager/Exception.php";

      throw new JooS_PHPackager_Exception($Message, $Code);
    }
  }
